/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements.  See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License.  You may obtain a copy of the License at
*
*	  http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sirona.tracking;

import org.apache.sirona.configuration.Configuration;
import org.apache.sirona.configuration.ioc.Destroying;
import org.apache.sirona.configuration.ioc.IoCs;
import org.apache.sirona.spi.Order;
import org.apache.sirona.spi.SPI;
import org.apache.sirona.store.DataStoreFactory;
import org.apache.sirona.store.tracking.PathTrackingDataStore;

import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Contains logic to track class#method invocation path
 */
public class PathTracker {
    private static final String NODE = Configuration.getProperty(
            Configuration.CONFIG_PROPERTY_PREFIX + "javaagent.path.tracking.marker", //
            Configuration.getProperty("org.apache.org.apache.sirona.cube.CubeBuilder.marker", "node"));

    private static final PathTrackingDataStore PATH_TRACKING_DATA_STORE = IoCs
            .findOrCreateInstance(DataStoreFactory.class).getPathTrackingDataStore();

    private static final ThreadLocal<Context> THREAD_LOCAL = new ThreadLocal<Context>() {
        @Override
        protected Context initialValue() {
            return new Context();
        }
    };

    private final PathTrackingInformation pathTrackingInformation;

    private static final boolean USE_EXECUTORS = Boolean.parseBoolean(
            Configuration.getProperty(Configuration.CONFIG_PROPERTY_PREFIX + "pathtracking.useexecutors", "false"));

    private static boolean USE_SINGLE_STORE = Boolean.parseBoolean(
            Configuration.getProperty(Configuration.CONFIG_PROPERTY_PREFIX + "pathtracking.singlestore", "false"));

    protected static ExecutorService EXECUTORSERVICE;

    static {

        if (USE_EXECUTORS) {
            int threadsNumber = Configuration
                    .getInteger(Configuration.CONFIG_PROPERTY_PREFIX + "pathtracking.executors", 5);
            EXECUTORSERVICE = Executors.newFixedThreadPool(threadsNumber);
        }
    }

    private static PathTrackingInvocationListener[] LISTENERS;

    static {
        ClassLoader classLoader = PathTracker.class.getClassLoader();

        if (classLoader == null) {
            classLoader = Thread.currentThread().getContextClassLoader();
        }

        List<PathTrackingInvocationListener> listeners = new ArrayList<PathTrackingInvocationListener>();

        Iterator<PathTrackingInvocationListener> iterator = SPI.INSTANCE
                .find(PathTrackingInvocationListener.class, classLoader).iterator();

        while (iterator.hasNext()) {
            try {
                listeners.add(IoCs.autoSet(iterator.next()));
            } catch (Exception e) {
                throw new RuntimeException(e.getMessage(), e);
            }
        }

        Collections.sort(listeners, ListenerComparator.INSTANCE);
        LISTENERS = listeners.toArray(new PathTrackingInvocationListener[listeners.size()]);
    }

    public static PathTrackingInvocationListener[] getPathTrackingInvocationListeners() {
        return LISTENERS;
    }

    private PathTracker(final PathTrackingInformation pathTrackingInformation) {
        this.pathTrackingInformation = pathTrackingInformation;
    }

    private static void cleanUp() {
        THREAD_LOCAL.remove();
    }

    // An other solution could be using Thread.currentThread().getStackTrace() <- very slow

    public static PathTracker start(PathTrackingInformation pathTrackingInformation) {

        final Context context = THREAD_LOCAL.get();

        int level = 0;
        final PathTrackingInformation current = context.getPathTrackingInformation();
        if (current == null) {
            level = context.getLevel().incrementAndGet();
            pathTrackingInformation.setLevel(level);
        } else {
            // same class so no inc
            if (current != pathTrackingInformation) {
                level = context.getLevel().incrementAndGet();
                pathTrackingInformation.setLevel(level);
                pathTrackingInformation.setParent(current);
            }

        }
        pathTrackingInformation.setStart(System.nanoTime());

        context.setPathTrackingInformation(pathTrackingInformation);

        for (PathTrackingInvocationListener listener : LISTENERS) {
            if (level == 1) {
                listener.startPath(context);
            } else {
                listener.enterMethod(context);
            }
        }

        return new PathTracker(pathTrackingInformation);
    }

    public void stop() {
        final long end = System.nanoTime();
        final long start = pathTrackingInformation.getStart();
        final Context context = THREAD_LOCAL.get();

        final String uuid = context.getUuid();

        final PathTrackingInformation current = context.getPathTrackingInformation();
        // same invocation so no inc, class can do recursion so don't use classname/methodname
        if (pathTrackingInformation != current) {
            context.getLevel().decrementAndGet();
            context.setPathTrackingInformation(pathTrackingInformation.getParent());
        }

        if (context.getPathTrackingInformation() != null) {
            for (PathTrackingInvocationListener listener : LISTENERS) {
                listener.exitMethod(context);

            }
        }

        final PathTrackingEntry pathTrackingEntry = new PathTrackingEntry(uuid, NODE,
                pathTrackingInformation.getClassName(), pathTrackingInformation.getMethodName(), start, (end - start),
                pathTrackingInformation.getLevel());
        if (USE_SINGLE_STORE) {
            PATH_TRACKING_DATA_STORE.store(pathTrackingEntry);
        } else {
            context.getEntries().add(pathTrackingEntry);
        }

        if (pathTrackingInformation.getLevel() == 1 && pathTrackingInformation.getParent() == null) { // 0
                                                                                                      // is
                                                                                                      // never
                                                                                                      // reached
                                                                                                      // so
                                                                                                      // 1
                                                                                                      // is
                                                                                                      // first
            if (!USE_SINGLE_STORE) {
                Runnable runnable = new Runnable() {
                    @Override
                    public void run() {
                        PATH_TRACKING_DATA_STORE.store(context.getEntries());
                        PathTracker.cleanUp();
                    }
                };
                if (USE_EXECUTORS) {
                    EXECUTORSERVICE.submit(runnable);
                } else {
                    runnable.run();
                }
            }

            for (PathTrackingInvocationListener listener : LISTENERS) {
                listener.endPath(context);
            }

        }
    }

    @Destroying
    public void destroy() {
        PathTracker.shutdown();
    }

    public static void shutdown() {
        EXECUTORSERVICE.shutdownNow();
    }

    private static class ListenerComparator implements Comparator<PathTrackingInvocationListener> {
        private static final ListenerComparator INSTANCE = new ListenerComparator();

        private ListenerComparator() {
            // no-op
        }

        @Override
        public int compare(final PathTrackingInvocationListener o1, final PathTrackingInvocationListener o2) {
            final Order order1 = o1.getClass().getAnnotation(Order.class);
            final Order order2 = o2.getClass().getAnnotation(Order.class);
            if (order2 == null) {
                return -1;
            }
            if (order1 == null) {
                return 1;
            }
            return order1.value() - order2.value();
        }
    }

}
